/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2009  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>

#include <glib.h>
#include <gdbus.h>

#include <connman/assert.h>

#include "connman.h"

#define PROFILE_DEFAULT_IDENT  "default"
#define	PROFILE_MAX		3		/* 2 is probably sufficient */

#define	PROFILE_DEFAULT_PORTAL_URL \
	"http://clients3.google.com/generate_204"

#define	_DBG_PROFILE(fmt, arg...)	DBG(DBG_PROFILE, fmt, ## arg)

struct connman_profile {
	struct connman_storage_ident ident;
	guint changed_timeout;
	char *path;
	char *name;
	connman_bool_t offlinemode;
	connman_bool_t arpgateway;
	char *country;
	uint32_t checkportal;
	char *portal_url;
};

static GHashTable *profile_hash = NULL;

static struct connman_profile *profile_stack[PROFILE_MAX];
static int cur_profile = -1;

static struct connman_storage_ident default_ident = {
	.ident = PROFILE_DEFAULT_IDENT
};
static struct connman_profile *default_profile = NULL;

static guint changed_timeout = 0;		/* for NULL profile handling */

static DBusConnection *connection = NULL;

/* NB: everything but UNKONWN and VPN */
static uint32_t profile_default_checkportal_mask =
	(0xffffffff &~
	    ((1<<CONNMAN_SERVICE_TYPE_UNKNOWN) | (1<<CONNMAN_SERVICE_TYPE_VPN)));

static const char *__profile_name(const struct connman_profile *profile)
{
	return (profile == NULL) ? "(null)" : profile->path;
}

/*
 * Loading/Saving objects.
 *
 * Service objects go to the profile they are pinned to (typically
 * the active profile at the time they were created but this can be
 * changed, e.g. from private -> global).
 *
 * Device and ipconfig objects go in the global profile (if any).
 * This ensures that enable/disable state is maintained between
 * users (and reboots); or possibly discarded (e.g. for testing).
 *
 * Likewise global state like offline mode is stored in the global
 * profile (see above).
 */

/*
 * Return the active profile; it's on the top of the stack.
 */
static inline struct connman_profile *active_profile(void)
{
	return cur_profile >= 0 ? profile_stack[cur_profile] : NULL;
}

/*
 * Return the global profile; it's top-most non-user profile.
 */
static struct connman_profile *global_profile(void)
{
	/* TODO(sleffler) cheat for now */
	return default_profile != NULL ? default_profile : NULL;
}

static int ident_equal(const struct connman_storage_ident *a,
		       const struct connman_storage_ident *b)
{
	return (g_strcmp0(a->user, b->user) == 0 &&
		g_strcmp0(a->ident, b->ident) == 0);
}

static void append_path(gpointer key, gpointer value, gpointer user_data)
{
	struct connman_profile *profile = value;
	DBusMessageIter *iter = user_data;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
							&profile->path);
}

void __connman_profile_list(DBusMessageIter *iter, void *arg)
{
	g_hash_table_foreach(profile_hash, append_path, iter);
}

static void profiles_changed(void)
{
	connman_dbus_send_property_changed_array(CONNMAN_MANAGER_PATH,
	    CONNMAN_MANAGER_INTERFACE, "Profiles",
	    DBUS_TYPE_OBJECT_PATH, __connman_profile_list, NULL);
}

connman_bool_t __connman_profile_get_offlinemode(void)
{
	struct connman_profile *profile = global_profile();
	return (profile == NULL) ? FALSE : profile->offlinemode;
}

int __connman_profile_set_offlinemode(connman_bool_t offlinemode)
{
	struct connman_profile *profile = global_profile();
	int ret;

	_DBG_PROFILE("offlinemode %d profile %s", offlinemode,
	    __profile_name(profile));

	/* NB: always succeeds (ATM) */
	ret = __connman_device_set_offlinemode(offlinemode);
	if (ret != 0)
		return ret;

	/* TODO(sleffler) sallow even if no global profile? */
	if (profile != NULL) {
		/*
		 * OfflineMode is only saved to the default profile;
		 * this ensures it is preserved across user changes.
		 */
		if (profile->offlinemode == offlinemode)
			return -EALREADY;

		profile->offlinemode = offlinemode;

		connman_dbus_send_property_changed_variant(
		    profile->path,
		    CONNMAN_PROFILE_INTERFACE, "OfflineMode",
		    DBUS_TYPE_BOOLEAN, &offlinemode);

		__connman_storage_save_profile(profile);
	}
	return 0;
}

connman_bool_t connman_profile_get_arpgateway(void)
{
	struct connman_profile *profile = global_profile();
	return (profile == NULL) ? FALSE : profile->arpgateway;
}

int connman_profile_set_arpgateway(connman_bool_t arpgateway)
{
	struct connman_profile *profile = global_profile();

	_DBG_PROFILE("arpgateway %d profile %s", arpgateway,
	    __profile_name(profile));

	if (profile != NULL) {
		if (profile->arpgateway == arpgateway)
			return -EALREADY;

		profile->arpgateway = arpgateway;

		connman_dbus_send_property_changed_variant(
		    profile->path,
		    CONNMAN_PROFILE_INTERFACE, "ArpGateway",
		    DBUS_TYPE_BOOLEAN, &arpgateway);

		__connman_storage_save_profile(profile);
	}
	return 0;
}

static void country_changed(struct connman_profile *profile)
{
	connman_dbus_send_property_changed_variant(profile->path,
	    CONNMAN_PROFILE_INTERFACE, "Country",
	    DBUS_TYPE_STRING, &profile->country);
}

const char *connman_profile_get_country(void)
{
	struct connman_profile *profile = global_profile();
	return (profile == NULL) ? NULL : profile->country;
}

int connman_profile_set_country(const char *country)
{
	struct connman_profile *profile = global_profile();

	_DBG_PROFILE("country %s profile %s", country, __profile_name(profile));

	if (profile != NULL) {
		if (g_strcmp0(profile->country, country) == 0)
			return -EALREADY;

		g_free(profile->country);
		profile->country = g_strdup(country);

		country_changed(profile);

		__connman_storage_save_profile(profile);
	}
	__connman_notifier_country_changed(country);
	return 0;
}

uint32_t __connman_profile_get_checkportal(void)
{
	struct connman_profile *profile = global_profile();
	return (profile == NULL) ?
	     profile_default_checkportal_mask : profile->checkportal;
}

int __connman_profile_set_checkportal(uint32_t checkportal)
{
	struct connman_profile *profile = global_profile();
	gchar *str;

	_DBG_PROFILE("checkportal 0x%x profile %s", checkportal,
	    __profile_name(profile));

	if (profile != NULL) {
		/*
		 * CheckPortal is only saved to the global profile;
		 * this ensures it is preserved across user changes.
		 */
		if (profile->checkportal == checkportal)
			return -EALREADY;

		profile->checkportal = checkportal;

		str = __connman_service_mask_to_list(checkportal);
		if (str != NULL) {
			connman_dbus_send_property_changed_variant(
			    profile->path,
			    CONNMAN_PROFILE_INTERFACE, "CheckPortalList",
			    DBUS_TYPE_STRING, &str);
			g_free(str);
		}

		__connman_storage_save_profile(profile);
	}
	return 0;
}

const char *__connman_profile_get_portal_url(void)
{
	struct connman_profile *profile = global_profile();
	/* NB: always return something not NULL */
	return (profile == NULL || profile->portal_url == NULL) ?
	    PROFILE_DEFAULT_PORTAL_URL : profile->portal_url;
}

int __connman_profile_set_portal_url(const char *url)
{
	struct connman_profile *profile = global_profile();

	_DBG_PROFILE("url %s profile %s", url, __profile_name(profile));

	/* NB: require "http" prefix (allows https too) */
	if (url == NULL || g_str_has_prefix(url, "http") == FALSE)
		return -EINVAL;

	if (profile != NULL) {
		if (g_strcmp0(profile->portal_url, url) == 0)
			return -EALREADY;

		g_free(profile->portal_url);
		profile->portal_url = g_strdup(url);

		connman_dbus_send_property_changed_variant(profile->path,
		    CONNMAN_PROFILE_INTERFACE, "PortalURL",
		    DBUS_TYPE_STRING, &url);

		__connman_storage_save_profile(profile);
	}
	return 0;
}

static inline int load_continue(int err)
{
	/* NB: ENXIO for no file, ESRCH for no entry */
	return (err == -ENXIO || err == -ESRCH);
}

int __connman_profile_load_service(struct connman_service *service)
{
	struct connman_profile *profile =
	    __connman_service_get_profile(service);
	int err, i;

	_DBG_PROFILE("service %p profile %s", service, __profile_name(profile));

	if (profile != NULL)
		return __connman_storage_load_service(service, &profile->ident);
	/*
	 * Not bound to a profile yet, search the stack for an
	 * entry and if found bind the profile to the service.
	 */
	err = 0;
	for (i = cur_profile; i >= 0; i--) {
		profile = profile_stack[i];
		err = __connman_storage_load_service(service, &profile->ident);
		if (err == 0) {
			_DBG_PROFILE("bind to profile %s", profile->path);
			__connman_service_set_profile(service, profile);
			return 0;
		}
		if (!load_continue(err))
			break;
	}
	return err;
}

/*
 * Search for the specified GUID in the profile stack and return
 * the associated object path suitable for instantiating an instance
 * of the associated object (currently only a connman_service).
 */
char *__connman_profile_find_guid(const char *guid)
{
	char *ident = NULL;
	int i;

	_DBG_PROFILE("guid %s", guid);

	for (i = cur_profile; i >= 0; i--) {
		struct connman_profile *profile = profile_stack[i];

		ident = __connman_storage_find_guid(guid, &profile->ident);
		if (ident != NULL) {
			_DBG_PROFILE("found %s in profile %s", ident,
			    profile->path);
			break;
		}
	}
	return ident;
}

/*
 * NB: This is like __connman_profile_load_service but defaults
 * the service to use the global profile.  This is used for
 * "device services" (see __connman_service_create_from_device).
 */
int __connman_profile_load_device_service(struct connman_service *service)
{
	struct connman_profile *profile =
	    __connman_service_get_profile(service);

	_DBG_PROFILE("service %p profile %s", service, __profile_name(profile));

	if (profile == NULL) {
		/*
		 * Not bound to a profile yet, bind to the global profile.
		 */
		profile = global_profile();
		if (profile == NULL) {
			_DBG_PROFILE("no global profile; cannot bind");
			return -ESRCH;
		}
		_DBG_PROFILE("bind to profile %s", profile->path);
		__connman_service_set_profile(service, profile);
	}
	return __connman_storage_load_service(service, &profile->ident);
}

int __connman_profile_save_service(struct connman_service *service)
{
	struct connman_profile *profile =
	    __connman_service_get_profile(service);

	_DBG_PROFILE("service %p profile %s", service, __profile_name(profile));

	if (profile == NULL) {
		/* not bound yet, bind to the active profile */
		profile = active_profile();
		_DBG_PROFILE("bind to profile %s", __profile_name(profile));
		__connman_service_set_profile(service, profile);
	}
	return (profile == NULL) ? 0 :
	    __connman_storage_save_service(service, &profile->ident);
}

int __connman_profile_load_provider(struct connman_provider *provider)
{
	struct connman_profile *profile =
	    __connman_provider_get_profile(provider);
	int err, i;

	_DBG_PROFILE("provider %p profile %s", provider,
	    __profile_name(profile));

	if (profile != NULL)
		return __connman_storage_load_provider(provider,
		    &profile->ident);
	/*
	 * Not bound to a profile yet, search the stack for an
	 * entry and if found bind the profile to the provider.
	 */
	err = 0;
	for (i = cur_profile; i >= 0; i--) {
		profile = profile_stack[i];
		err = __connman_storage_load_provider(provider, &profile->ident);
		if (err == 0) {
			_DBG_PROFILE("bind to profile %s", profile->path);
			__connman_provider_set_profile(provider, profile);
			return 0;
		}
		if (!load_continue(err))
			break;
	}
	return err;
}

int __connman_profile_save_provider(struct connman_provider *provider)
{
	struct connman_profile *profile =
	    __connman_provider_get_profile(provider);

	_DBG_PROFILE("provider %p profile %s", provider,
	    __profile_name(profile));

	if (profile == NULL) {
		/* not bound yet, bind to the active profile */
		profile = active_profile();
		_DBG_PROFILE("bind to profile %s", profile->path);
		__connman_provider_set_profile(provider, profile);
	}
	return (profile == NULL) ? 0 :
	    __connman_storage_save_provider(provider, &profile->ident);
}

int __connman_profile_load_device(struct connman_device *device)
{
	struct connman_profile *profile = global_profile();

	_DBG_PROFILE("device %p profile %s", device, __profile_name(profile));

	return (profile == NULL) ? 0 :
	    __connman_storage_load_device(device, &profile->ident);
}

int __connman_profile_save_device(struct connman_device *device)
{
	struct connman_profile *profile = global_profile();

	_DBG_PROFILE("device %p profile %s", device, __profile_name(profile));

	return (profile == NULL) ? 0:
	    __connman_storage_save_device(device, &profile->ident);
}

int __connman_profile_load_ipconfig(struct connman_ipconfig *ipconfig)
{
	struct connman_profile *profile = global_profile();

	_DBG_PROFILE("ipconfig %p profile %s", ipconfig,
	    __profile_name(profile));

	return (profile == NULL) ? 0:
	    __connman_storage_load_ipconfig(ipconfig, &profile->ident);
}

int __connman_profile_save_ipconfig(const struct connman_ipconfig *ipconfig)
{
	struct connman_profile *profile = global_profile();

	_DBG_PROFILE("ipconfig %p profile %s", ipconfig,
	    __profile_name(profile));

	return (profile == NULL) ? 0 :
	    __connman_storage_save_ipconfig(ipconfig, &profile->ident);
}

int __connman_profile_append_hidden_ssids(GSList **hidden_ssids,
    void (*append_hidden_ssids)(GKeyFile *keyfile, GSList **hidden_ssids))
{
	int i;

	_DBG_PROFILE("");

	for (i = cur_profile; i >= 0; i--) {
		const struct connman_profile *profile = profile_stack[i];
		GKeyFile *keyfile;

		keyfile = __connman_storage_open(&profile->ident);
		if (keyfile != NULL) {
			append_hidden_ssids(keyfile, hidden_ssids);
			__connman_storage_close(&profile->ident, keyfile,
			    FALSE);
		}
	}
	return 0;
}

/*
 * Return the object path for the specified profile.
 */
const char *__connman_profile_get_path(const struct connman_profile *profile)
{
	return profile != NULL ? profile->path : NULL;
}

/*
 * Return the profile given an object path.
 */
struct connman_profile *__connman_profile_lookup_profile(const char *path)
{
	return g_hash_table_lookup(profile_hash, path);
}

/*
 * Return the active profile or NULL
 */
struct connman_profile *__connman_profile_active_profile(void)
{
	return active_profile();
}

const struct connman_storage_ident *__connman_profile_active_ident(void)
{
	struct connman_profile *profile = active_profile();
	return profile != NULL ? &profile->ident : NULL;
}

/*
 * Return the object path for the active profile or NULL
 * if there is none.
 */
const char *__connman_profile_active_path(void)
{
	struct connman_profile *profile = active_profile();
	return profile != NULL ? profile->path : NULL;
}

/*
 * Delete an entry in the specified profile.
 */
int __connman_profile_delete_entry(struct connman_profile *profile,
    const char *group)
{
	GKeyFile *keyfile;
	gboolean status;

	_DBG_PROFILE("profile %s group %s", __profile_name(profile), group);

	keyfile = __connman_storage_open(&profile->ident);
	if (keyfile == NULL) {
		_DBG_PROFILE("cannot open key file");
		return -EINVAL;
	}

	status = g_key_file_remove_group(keyfile, group, NULL);
	__connman_storage_close(&profile->ident, keyfile, status);

	if (status == FALSE) {
		_DBG_PROFILE("cannot remove %s", group);
		return -ENXIO;
	}
	return 0;
}

static void __clear_timeout(guint *pchanged_timeout)
{
	if (*pchanged_timeout > 0) {
		g_source_remove(*pchanged_timeout);
		*pchanged_timeout = 0;
	}
}

static void clear_timeout(struct connman_profile *profile)
{
	if (profile != NULL)
		__clear_timeout(&profile->changed_timeout);
	else
		__clear_timeout(&changed_timeout);
}

static void append_services(DBusMessageIter *iter, void *arg)
{
	__connman_service_list(iter, arg);
}
static gboolean services_changed(gpointer user_data)
{
	struct connman_profile *profile = user_data;

	if (profile != NULL) {
		profile->changed_timeout = 0;

		connman_dbus_send_property_changed_array(profile->path,
		    CONNMAN_PROFILE_INTERFACE, "Services",
		    DBUS_TYPE_OBJECT_PATH, append_services, NULL);
	} else
		changed_timeout = 0;

	connman_dbus_send_property_changed_array(CONNMAN_MANAGER_PATH,
	    CONNMAN_MANAGER_INTERFACE, "Services",
	    DBUS_TYPE_OBJECT_PATH, append_services, NULL);
	return FALSE;
}

/*
 * Handle changes to a profile.  Generate PropertyChanged signals
 * on Manager.Services and Profile.Services for the currently active
 * profile.  To minimize overhead requests may be coalesced using a
 * 1 second delay on the signals.
 */
void __connman_profile_changed(struct connman_profile *profile,
    gboolean delayed)
{
	_DBG_PROFILE("profile %s%s changed_timeout %d",
	    __profile_name(profile), delayed ? " delayed" : "",
	        profile != NULL ? profile->changed_timeout : changed_timeout);

	clear_timeout(profile);

	if (delayed == TRUE) {
		guint timeout = g_timeout_add_seconds(1,
		    services_changed, profile);
		if (profile != NULL)
			profile->changed_timeout = timeout;
		else
			changed_timeout = timeout;
	} else
		services_changed(profile);
}

int __connman_profile_add_device(struct connman_device *device)
{
	_DBG_PROFILE("device %p", device);

	return __connman_service_create_from_device(device);
}

int __connman_profile_remove_device(struct connman_device *device)
{
	_DBG_PROFILE("device %p", device);

	__connman_service_remove_from_device(device);

	return 0;
}

int __connman_profile_add_network(struct connman_network *network)
{
	_DBG_PROFILE("network %p", network);

	return __connman_service_create_from_network(network);
}

int __connman_profile_update_network(struct connman_network *network)
{
	_DBG_PROFILE("network %p", network);

	__connman_service_update_from_network(network);

	return 0;
}

int __connman_profile_remove_network(struct connman_network *network)
{
	_DBG_PROFILE("network %p", network);

	__connman_service_remove_from_network(network);

	return 0;
}

/*
 * Extract the service type from a profile group name
 * and return the enumerated type value (if any).  Return
 * CONNMAN_SERVICE_TYPE_UNKNOWN for group names we don't
 * care about.
 */
static int get_service_type(const char *gname)
{
	char type[32];		/* NB: known large enough */
	int i;

	/* extract type from group name */
	for (i = 0; gname[i] != '_'; i++) {
		if (gname[i] == '\0')
			return CONNMAN_SERVICE_TYPE_UNKNOWN;
		if (i >= sizeof(type)-1) /* -1 for \0 */
			return CONNMAN_SERVICE_TYPE_UNKNOWN;
		type[i] = gname[i];
	}
	type[i] = '\0';
	return __connman_service_string2type(type);
}

static void __profile_entry_list(DBusMessageIter *iter, void *arg)
{
	struct connman_profile *profile = arg;
	GKeyFile *keyfile;
	gchar **groups;
	int i;

	_DBG_PROFILE("profile %s", __profile_name(profile));

	keyfile = __connman_storage_open(&profile->ident);
	if (keyfile == NULL)
		return;

	groups = g_key_file_get_groups(keyfile, NULL);
	for (i = 0; groups[i] != NULL; i++) {
		const char *gname = groups[i];
		switch (get_service_type(gname)) {
		case CONNMAN_SERVICE_TYPE_UNKNOWN:
		case CONNMAN_SERVICE_TYPE_ETHERNET:
			break;
		case CONNMAN_SERVICE_TYPE_BLUETOOTH:
		case CONNMAN_SERVICE_TYPE_WIMAX:
			/* NB: don't care about these right now */
			break;
		case CONNMAN_SERVICE_TYPE_CELLULAR:
		case CONNMAN_SERVICE_TYPE_WIFI:
		case CONNMAN_SERVICE_TYPE_VPN:
			dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
			    &gname);
			break;
		}
	}
	g_strfreev(groups);
	__connman_storage_close(&profile->ident, keyfile, FALSE);
}

static DBusMessage *get_properties(DBusConnection *conn,
					DBusMessage *msg, void *data)
{
	struct connman_profile *profile = data;
	DBusMessage *reply;
	DBusMessageIter array, dict;

	_DBG_PROFILE("conn %p", conn);

	reply = dbus_message_new_method_return(msg);
	if (reply == NULL)
		return NULL;

	dbus_message_iter_init_append(reply, &array);

	dbus_message_iter_open_container(&array, DBUS_TYPE_ARRAY,
			DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
			DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING
			DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict);

	if (profile->name != NULL)
		connman_dbus_dict_append_variant(&dict, "Name",
		    DBUS_TYPE_STRING, &profile->name);

	if (profile == global_profile()) {
		gchar *str;

		connman_dbus_dict_append_variant(&dict, "OfflineMode",
		    DBUS_TYPE_BOOLEAN, &profile->offlinemode);
		if (profile->country != NULL)
			connman_dbus_dict_append_variant(&dict, "Country",
			    DBUS_TYPE_STRING, &profile->country);
		str = __connman_service_mask_to_list(profile->checkportal);
		if (str != NULL) {
			connman_dbus_dict_append_variant(&dict,
			    "CheckPortalList", DBUS_TYPE_STRING, &str);
			g_free(str);
		}
		if (profile->portal_url != NULL)
			connman_dbus_dict_append_variant(&dict, "PortalURL",
			    DBUS_TYPE_STRING, &profile->portal_url);
		connman_dbus_dict_append_variant(&dict, "ArpGateway",
		    DBUS_TYPE_BOOLEAN, &profile->arpgateway);
	}

	if (profile == active_profile())
		connman_dbus_dict_append_variant_array(&dict, "Services",
		    DBUS_TYPE_OBJECT_PATH, __connman_service_list, NULL);

	connman_dbus_dict_append_variant_array(&dict, "Entries",
	    DBUS_TYPE_STRING, __profile_entry_list, profile);

	dbus_message_iter_close_container(&array, &dict);

	return reply;
}

static DBusMessage *set_property(DBusConnection *conn,
					DBusMessage *msg, void *data)
{
	struct connman_profile *profile = data;
	DBusMessageIter iter, value;
	const char *name;
	int type;

	_DBG_PROFILE("conn %p", conn);

	if (dbus_message_iter_init(msg, &iter) == FALSE)
		return __connman_error_invalid_arguments(msg);

	dbus_message_iter_get_basic(&iter, &name);
	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &value);

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_MODIFY) < 0)
		return __connman_error_permission_denied(msg);

	type = dbus_message_iter_get_arg_type(&value);

	if (g_str_equal(name, "Name") == TRUE) {
		const char *name;

		if (type != DBUS_TYPE_STRING)
			return __connman_error_invalid_arguments(msg);

		dbus_message_iter_get_basic(&value, &name);

		g_free(profile->name);
		profile->name = g_strdup(name);

		if (profile->name != NULL) {
			connman_dbus_send_property_changed_variant(
			    profile->path, CONNMAN_PROFILE_INTERFACE, "Name",
			    DBUS_TYPE_STRING, &profile->name);
		}

		__connman_storage_save_profile(profile);
	} else
		return __connman_error_invalid_property(msg);

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static void __append_str(DBusMessageIter *dict, const char *name,
    GKeyFile *keyfile, const char *ident)
{
	char *val = g_key_file_get_string(keyfile, ident, name, NULL);
	if (val != NULL) {
		connman_dbus_dict_append_variant(dict, name,
		    DBUS_TYPE_STRING, &val);
		g_free(val);
	}
}

static void __append_bool(DBusMessageIter *dict, const char *name,
    GKeyFile *keyfile, const char *ident)
{
	char *val = g_key_file_get_string(keyfile, ident, name, NULL);
	if (val != NULL) {
		connman_bool_t b = (g_strcmp0(val, "true") == 0 ? TRUE : FALSE);
		connman_dbus_dict_append_variant(dict, name,
		    DBUS_TYPE_BOOLEAN, &b);
		g_free(val);
	}
}

static DBusMessage *get_entry(DBusConnection *conn,
					DBusMessage *msg, void *data)
{
	struct connman_profile *profile = data;
	GKeyFile *keyfile;
	const char *ident, *val;
	gchar **tokens;
	DBusMessageIter iter, array, dict;
	DBusMessage *reply;
	int len;

	if (dbus_message_iter_init(msg, &iter) == FALSE)
		return __connman_error_invalid_arguments(msg);

	dbus_message_iter_get_basic(&iter, &ident);

	keyfile = __connman_storage_open(&profile->ident);
	if (keyfile == NULL) {
		_DBG_PROFILE("cannot open keyfile %s", __profile_name(profile));
		return __connman_error_not_found(msg);	/* XXX */
	}

	if (g_key_file_has_group(keyfile, ident) == FALSE) {
		_DBG_PROFILE("keyfile %s ident %s not found",
		    __profile_name(profile), ident);
		__connman_storage_close(&profile->ident, keyfile, FALSE);
		return __connman_error_not_found(msg);
	}

	_DBG_PROFILE("profile %s ident %s", __profile_name(profile), ident);

	/*
	 * Split group name into components; e.g.
	 * <type>_<device>_<ssid>_<mode>_<security> for wifi
	 */
	tokens = g_strsplit(ident, "_", 0);
	len = g_strv_length(tokens);
	if (len < 2) {
		_DBG_PROFILE("profile %s ident %s malformed, len %d",
		    __profile_name(profile), ident, len);
		g_strfreev(tokens);
		__connman_storage_close(&profile->ident, keyfile, FALSE);
		return __connman_error_invalid_arguments(msg);
	}

	reply = dbus_message_new_method_return(msg);
	if (reply == NULL) {
		_DBG_PROFILE("cannot allocate reply");
		g_strfreev(tokens);
		__connman_storage_close(&profile->ident, keyfile, FALSE);
		return NULL;
	}

	dbus_message_iter_init_append(reply, &array);

	dbus_message_iter_open_container(&array, DBUS_TYPE_ARRAY,
			DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
			DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING
			DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict);

	val = tokens[0];
	connman_dbus_dict_append_variant(&dict, "Type", DBUS_TYPE_STRING, &val);

	__append_str(&dict, "Name", keyfile, ident);
	__append_bool(&dict, "AutoConnect", keyfile, ident);
	__append_str(&dict, "Failure", keyfile, ident);
	__append_str(&dict, "Modified", keyfile, ident);
	__append_str(&dict, "Passphrase", keyfile, ident);
	__append_str(&dict, "UIData", keyfile, ident);
	__append_str(&dict, "GUID", keyfile, ident);

	switch (__connman_service_string2type(tokens[0])) {
	case CONNMAN_SERVICE_TYPE_WIFI:
		if (len != 5 && len != 6) {
			_DBG_PROFILE("profile %s ident %s bad token cnt %d",
			    __profile_name(profile), ident, len);
			break;
		}
		val = tokens[3];
		connman_dbus_dict_append_variant(&dict, "Mode",
		   DBUS_TYPE_STRING, &val);
		val = tokens[4];
		/* NB: g_strsplit breaks 802_1x into 802+1x; restore */
		if (g_strcmp0(val, "802") == 0)
			val = "802_1x";
		connman_dbus_dict_append_variant(&dict, "Security",
		   DBUS_TYPE_STRING, &val);

		__append_bool(&dict, "WiFi.HiddenSSID", keyfile, ident);
		break;
	case CONNMAN_SERVICE_TYPE_VPN: {
		struct __connman_provider_append_profile_properties_args args;

		args.keyfile = keyfile;
		args.ident = ident;
		connman_dbus_dict_append_dict(&dict, "Provider",
		    __connman_provider_append_profile_properties, &args);
		break;
	}
	case CONNMAN_SERVICE_TYPE_CELLULAR:
		/* TODO(sleffler) extract cellular attributes */
		break;
	default:
		break;
	}

	dbus_message_iter_close_container(&array, &dict);

	g_strfreev(tokens);
	__connman_storage_close(&profile->ident, keyfile, FALSE);

	return reply;
}

static DBusMessage *delete_entry(DBusConnection *conn,
					DBusMessage *msg, void *data)
{
	struct connman_profile *profile = data;
	DBusMessageIter iter;
	const char *identifier;
	struct connman_service *service;
	int err;

	if (dbus_message_iter_init(msg, &iter) == FALSE)
		return __connman_error_invalid_arguments(msg);

	dbus_message_iter_get_basic(&iter, &identifier);

	_DBG_PROFILE("profile %s:%s %s", profile->ident.user,
	    profile->ident.ident, identifier);

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_MODIFY) < 0)
		return __connman_error_permission_denied(msg);

	service = __connman_service_lookup(identifier);
	if (service != NULL) {
		__connman_service_disconnect(service);

		/* NB: this does not remove the service */
		__connman_service_reset_in_memory(service);
		__connman_service_set_profile(service, NULL);
	}

	/* Remove directly from profile */
	err = __connman_profile_delete_entry(profile, identifier);
	if (err)
		return __connman_error_failed(msg, -err);

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static GDBusMethodTable profile_methods[] = {
	{ "GetProperties", "",   "a{sv}", get_properties },
	{ "SetProperty",   "sv", "",      set_property   },
	{ "GetEntry",      "s",  "a{sv}", get_entry },
	{ "DeleteEntry",   "s",  "",      delete_entry },
	{ },
};

static GDBusSignalTable profile_signals[] = {
	{ "PropertyChanged", "sv" },
	{ },
};

static void free_ident(struct connman_storage_ident *ident)
{
	/* NB: blech, don't reclaim, it's statically allocated */
	if (!ident_equal(ident, &default_ident)) {
		g_free(ident->user);
		g_free(ident->ident);
	}
}

static void free_profile(struct connman_profile *profile)
{
	free_ident(&profile->ident);
	g_free(profile->path);
	g_free(profile->name);
	g_free(profile);
}

static void unregister_profile(gpointer data)
{
	struct connman_profile *profile = data;

	connman_info("Remove profile %s:%s", profile->ident.user,
	    profile->ident.ident);

	g_dbus_unregister_interface(connection, profile->path,
						CONNMAN_PROFILE_INTERFACE);

	clear_timeout(profile);

	if (profile == default_profile)
		default_profile = NULL;

	free_profile(profile);
}

static char *getpath(const struct connman_storage_ident *ident)
{
	if (ident->user != NULL) {
		/* NB: check for two tokens done in validate_ident */
		return g_strdup_printf("/profile/%s/%s",
		    ident->user, ident->ident);
	} else
		return g_strdup_printf("/profile/%s", ident->ident);
}

/*
 * Create an in-memory profile using the specified identifier
 * and name.  The object path and a reference to the profile
 * data structure are returned on success.
 */
static int create_profile(struct connman_storage_ident *ident,
    const char *name, const char **path, struct connman_profile **pprofile)
{
	struct connman_profile *profile;

	_DBG_PROFILE("ident %s:%s name %s", ident->user, ident->ident, name);

	profile = g_try_new0(struct connman_profile, 1);
	if (profile == NULL) {
		free_ident(ident);
		return -ENOMEM;
	}

	profile->ident = *ident;
	profile->path = getpath(ident);
	/* TODO(sleffler) check ident.user */
	if (profile->ident.ident == NULL || profile->path == NULL) {
		free_profile(profile);
		return -ENOMEM;
	}

	if (g_hash_table_lookup(profile_hash, profile->path) != NULL) {
		free_profile(profile);
		return -EEXIST;
	}

	profile->checkportal = profile_default_checkportal_mask;
	profile->name = g_strdup(name);

	__connman_storage_load_profile(profile);

	g_hash_table_insert(profile_hash, g_strdup(profile->path), profile);

	connman_info("Add profile %s:%s", ident->user, ident->ident);

	if (ident_equal(ident, &default_ident))
		default_profile = profile;

	g_dbus_register_interface(connection, profile->path,
					CONNMAN_PROFILE_INTERFACE,
					profile_methods, profile_signals,
							NULL, profile, NULL);

	if (path != NULL)
		*path = profile->path;

	_DBG_PROFILE("profile %p path %s", profile, profile->path);

	*pprofile = profile;
	return 0;
}

/*
 * Check a profile identifier token.  This must be non-null and
 * made up of alpha-numeric chars suitable for use in a D-bus
 * object path.
 */
static gboolean validate_token(const char *ident)
{
	unsigned int i;
	unsigned int len = strlen(ident);

	if (len < 1)
		return FALSE;

	for (i = 0; i < len; i++) {
		if (ident[i] >= '0' && ident[i] <= '9')
			continue;
		if (ident[i] >= 'a' && ident[i] <= 'z')
			continue;
		if (ident[i] >= 'A' && ident[i] <= 'Z')
			continue;
		return FALSE;
	}

	return TRUE;
}

/*
 * Validate the profile identifier.  It must be suitable
 * for use in a D-Bus object path and, optionally, be of
 * the form ~user/ident to signify a per-user profile.
 */ 
static gboolean parse_ident(const char *str,
    struct connman_storage_ident *ident)
{
	gboolean is_valid;

	if (str[0] == '~') {
		/* syntax is ~user/name for profile in cryptohome */
		gchar **tokens = g_strsplit_set(str, "~/", 4);
		if (g_strv_length(tokens) == 3) {
			is_valid = (validate_token(tokens[1]) == TRUE &&
				    validate_token(tokens[2]) == TRUE);
			if (is_valid) {
				ident->user = g_strdup(tokens[1]);
				ident->ident = g_strdup(tokens[2]);
			}
		} else
			is_valid = FALSE;
		g_strfreev(tokens);
	} else {
		is_valid = validate_token(str);
		if (is_valid) {
			ident->user = NULL;
			ident->ident = g_strdup(str);
		}
	}
	return is_valid;
}

/*
 * Lookup a profile on the stack by object path.
 */
static struct connman_profile *lookup_stack_by_path(const char *path)
{
	int i;

	for (i = cur_profile; i >= 0; i--)
		if (g_strcmp0(profile_stack[i]->path, path) == 0)
			return profile_stack[i];
	return NULL;
}

/*
 * Push a profile on the stack and make it the ``active profile''.
 * The profile may be currently registered in memory or previously
 * created by CreateProfile.  The profile may not already be on
 * the stack.
 */
int __connman_profile_push(const char *ident, const char *name,
    const char **path)
{
	struct connman_profile *profile;
	struct connman_storage_ident sid;
	char *tmp_path;
	int err;

	_DBG_PROFILE("ident %s name %s", ident, name);

	if (parse_ident(ident, &sid) == FALSE) {
		connman_error("%s: invalid profile name %s", __func__, ident);
		return -EINVAL;
	}

	if (cur_profile+1 >= PROFILE_MAX) {
		connman_error("%s: too many profiles (max %d)", __func__,
		    PROFILE_MAX);
		free_ident(&sid);
		return -EMFILE;
	}

	/*
	 * Check for in-memory profile by way of a CreateProfile request.
	 */
	tmp_path = getpath(&sid);
	if (tmp_path == NULL) {
		connman_error("%s: no memory for %s", __func__, ident);
		free_ident(&sid);
		return -ENOMEM;
	}

	profile = g_hash_table_lookup(profile_hash, tmp_path);
	g_free(tmp_path);
	if (profile == NULL) {
		/*
		 * Not in memory; accept an existing file.
		 */
		if (__connman_storage_exists(&sid) == FALSE) {
			connman_error("%s: profile %s does not exist",
			    __func__, ident);
			free_ident(&sid);
			return -ENOENT;
		}

		err = create_profile(&sid, name, path, &profile);
		if (err < 0) {
			connman_error("%s: cannot open, error %d",
			    __func__, err);
			/* NB: create_profile reclaims sid */
			return err;
		}
	} else {
		free_ident(&sid);	/* NB: not needed below */
		/*
		 * Check this profile is not already on the stack.
		 */
		if (lookup_stack_by_path(profile->path) != NULL) {
			connman_error("%s: already pushed", __func__);
			return -EEXIST;
		}
		if (path != NULL)
			*path = profile->path;
	}

	profile_stack[++cur_profile] = profile;

	profiles_changed();

	__connman_notifier_profile_push(profile);

	return 0;
}

/*
 * Pop the profile from the top of the stack and remove it from
 * the in-memory table.  Any associated services are invalidated
 * (credentials revoked and connections closed). After a pop we
 * generate a PropertyChanged signal for Manager.Profiles.
 *
 * An optional identifier may be specified.  If specified it is
 * checked against the active profile and if not the same then
 * the request is rejected.  This is useful to ensure the right
 * profile is pop'd (as might happen on logout if an associated
 * push failed for some reason).
 */
int __connman_profile_pop(const char *ident)
{
	struct connman_profile *profile;

	_DBG_PROFILE("ident %s", ident);

	if (cur_profile < 0) {
		connman_error("%s: profile stack empty", __func__);
		return -EINVAL;
	}

	profile = profile_stack[cur_profile];

	if (ident != NULL) {
		struct connman_storage_ident sid;

		if (parse_ident(ident, &sid) == FALSE) {
			connman_error("%s: invalid profile name %s",
			    __func__, ident);
			return -EINVAL;
		}
		if (ident_equal(&profile->ident, &sid) == FALSE) {
			connman_error("%s: %s is not the active profile",
			    __func__, ident);
			free_ident(&sid);
			return -ENXIO;
		}
		free_ident(&sid);
	}

	/*
	 * Pop the stack, invalidate all objects holding references
	 * to the profile, then remove it from the in-memory table.
	 * We do the reclaim after notification in case anyone holding
	 * a reference tries to use it.
	 */
	cur_profile--;

	__connman_notifier_profile_pop(profile);

	g_hash_table_remove(profile_hash, profile->path);

	profiles_changed();

	return 0;
}

/*
 * Create a profile file and register it in memory.  The
 * file is created with minimal contents.
 * TODO(sleffler) disallow overwriting an existing file?
 */
int __connman_profile_create(const char *name, const char **path)
{
	struct connman_profile *profile;
	struct connman_storage_ident sid;
	int err;

	_DBG_PROFILE("name %s", name);

	if (parse_ident(name, &sid) == FALSE) {
		connman_error("%s: invalid profile name %s)", __func__, name);
		return -EINVAL;
	}

	err = create_profile(&sid, NULL, path, &profile);
	if (err < 0) {
		connman_error("%s: cannot open, error %d", __func__, err);
		/* NB: create_profile reclaims sid */
		return err;
	}

	__connman_storage_save_profile(profile);

	profiles_changed();

	return 0;
}

/*
 * Delete a profile and remove from memory.  The default
 * profile may not be removed/deleted.  Likewise, one
 * cannot remove a profile pushed on the stack.
 */
int __connman_profile_remove(const char *ident)
{
	struct connman_profile *profile;
	struct connman_storage_ident sid;
	char *tmp_path;
	int err = 0;

	_DBG_PROFILE("ident %s", ident);

	if (parse_ident(ident, &sid) == FALSE) {
		connman_error("%s: invalid profile name %s)", __func__, ident);
		return -EINVAL;
	}

	if (ident_equal(&sid, &default_ident)) {
		/* TODO(sleffler) should this be permitted? */
		connman_error("%s: cannot delete default profile", __func__);
		err = -EINVAL;
		goto done;
	}

	/*
	 * Check for in-memory profile by way of a CreateProfile request.
	 */
	tmp_path = getpath(&sid);
	if (tmp_path == NULL) {
		connman_error("%s: no memory for %s", __func__, ident);
		err = -ENOMEM;
		goto done;
	}
	profile = g_hash_table_lookup(profile_hash, tmp_path);
	g_free(tmp_path);

	if (profile != NULL) {
		/*
		 * Check this profile is not on the stack.
		 */
		if (lookup_stack_by_path(profile->path) != NULL) {
			connman_error("%s: cannot delete (on the stack)",
			    __func__);
			err = -EEXIST;
			goto done;
		}
		g_hash_table_remove(profile_hash, profile->path);

		profiles_changed();
	}

	__connman_storage_delete(&sid);
done:
	free_ident(&sid);
	return err;
}

/*
 * Initialize the profile stack by pushing the default
 * (global) profile.  Unlike connman we do not read in
 * all available .profile's in the global dir.
 */
static int profile_init(void)
{
	struct connman_profile *profile;
	int err;

	err = create_profile(&default_ident, "Default", NULL, &profile);
	if (err == 0)
		profile_stack[++cur_profile] = profile;
	return err;
}

static int profile_load(struct connman_profile *profile)
{
	GKeyFile *keyfile;
	GError *error = NULL;
	connman_bool_t offlinemode;
	connman_bool_t arpgateway;
	char *name, *country, *str;

	_DBG_PROFILE("profile %s", __profile_name(profile));

	keyfile = __connman_storage_open(&profile->ident);
	if (keyfile == NULL)
		return -EIO;

	name = g_key_file_get_string(keyfile, "global", "Name", NULL);
	if (name != NULL) {
		g_free(profile->name);
		profile->name = name;
	}

	country = g_key_file_get_string(keyfile, "global", "Country", NULL);
	if (country != NULL) {
		g_free(profile->country);
		profile->country = country;
	}

	str = g_key_file_get_string(keyfile, "global", "CheckPortalList", NULL);
	if (str != NULL) {
		profile->checkportal = __connman_service_list_to_mask(str);
		g_free(str);
	} else
		profile->checkportal = profile_default_checkportal_mask;

	str = g_key_file_get_string(keyfile, "global", "PortalURL", NULL);
	if (str != NULL) {
		g_free(profile->portal_url);
		profile->portal_url = str;
	}

	offlinemode = g_key_file_get_boolean(keyfile, "global",
						"OfflineMode", &error);
	if (error == NULL)
		profile->offlinemode = offlinemode;
	g_clear_error(&error);


	arpgateway = g_key_file_get_boolean(keyfile, "global",
					    "ArpGateway", &error);
	if (error == NULL)
		profile->arpgateway = arpgateway;
	else
		profile->arpgateway = TRUE;

	g_clear_error(&error);

	__connman_storage_close(&profile->ident, keyfile, FALSE);

	return 0;
}

static int profile_save(struct connman_profile *profile)
{
	GKeyFile *keyfile;
	gchar *str;

	_DBG_PROFILE("profile %s", __profile_name(profile));

	keyfile = __connman_storage_open(&profile->ident);
	if (keyfile == NULL)
		return -EIO;

	if (profile->name != NULL)
		g_key_file_set_string(keyfile, "global",
						"Name", profile->name);

	if (profile->country != NULL)
		g_key_file_set_string(keyfile, "global",
						"Country", profile->country);

	str = (profile->checkportal != profile_default_checkportal_mask)?
		__connman_service_mask_to_list(profile->checkportal):NULL;
	if (str != NULL) {
		g_key_file_set_string(keyfile, "global",
						"CheckPortalList", str);
		g_free(str);
	}

	if (profile->portal_url != NULL)
		g_key_file_set_string(keyfile, "global",
					"PortalURL", profile->portal_url);

	g_key_file_set_boolean(keyfile, "global",
					"OfflineMode", profile->offlinemode);

	g_key_file_set_boolean(keyfile, "global",
					"ArpGateway", profile->arpgateway);

	__connman_storage_close(&profile->ident, keyfile, TRUE);

	return 0;
}

static struct connman_storage profile_storage = {
	.name		= "profile",
	.priority	= CONNMAN_STORAGE_PRIORITY_LOW,
	.profile_init	= profile_init,
	.profile_load	= profile_load,
	.profile_save	= profile_save,
};

int __connman_profile_init(void)
{
	const char *portal_list;

	_DBG_PROFILE("");

	connection = connman_dbus_get_connection();
	if (connection == NULL)
		return -1;

	if (connman_storage_register(&profile_storage) < 0)
		connman_error("Failed to register profile storage");

	profile_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
						g_free, unregister_profile);

	portal_list = connman_option_get_string("portal-list");
	if (portal_list != NULL)
		profile_default_checkportal_mask =
			__connman_service_list_to_mask(portal_list);

	return 0;
}

int __connman_profile_push_batch(char **profiles)
{
	int i;

	if (profiles == NULL)
		return 0;

	for (i = 0; profiles[i] != NULL; i++) {
		int err = __connman_profile_push(profiles[i], NULL, NULL);
		if (err != 0)
			return err;
	}
	return 0;
}

void __connman_profile_cleanup(void)
{
	_DBG_PROFILE("");

	if (connection == NULL)
		return;

	while (cur_profile >= 0)
		__connman_profile_pop(NULL);

	clear_timeout(NULL);		/* NB: clear global timer */

	g_hash_table_destroy(profile_hash);
	profile_hash = NULL;

	connman_storage_unregister(&profile_storage);

	dbus_connection_unref(connection);
}
